#include "Server.h"

#include <folly/logging/xlog.h>

#include <bits/stdc++.h>

namespace nexus {

Server::Server(
    uint16_t port,
    Pipeline& pipeline,
    BlockingQueue<TopologyUpdate>& topologyUpdatesQueue,
    std::map<std::string, Network>& networks)
    : port_(port),
      pipeline_(pipeline),
      topologyUpdatesQueue_(topologyUpdatesQueue),
      networks_(networks) {
  nanobdd::init(1000000, 10000000, 32);
}

Server::~Server() {
  stop();
}

void
Server::run() {
  grpc::ServerBuilder builder;
  std::string addr = absl::StrFormat("0.0.0.0:%d", port_);
  builder.AddListeningPort(addr, grpc::InsecureServerCredentials());
  auto srv = ServiceImpl(this);
  builder.RegisterService(&srv);
  server_ = builder.BuildAndStart();
  std::cout << "Server listening on port " << port_ << std::endl;
  server_->Wait();
}

void
Server::stop() {
  server_->Shutdown();
}

grpc::Status
ServiceImpl::CreateNetwork(
    grpc::ServerContext* ctx,
    const proto::Network* network,
    proto::Response* resp) {
  XLOG(INFO) << "CreateNetwork";
  // Network n(network->name());
  // n.setToken(network->token());
  server_->networks_.emplace(network->name(), network->name());
  server_->networks_.at(network->name()).setToken(network->token());

  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::AddDevice(
    grpc::ServerContext* ctx,
    const proto::Device* device,
    proto::Response* resp) {
  XLOG(INFO) << "add dev: " << device->name();
  auto& dev =
      server_->networks_.at(device->network()).addDevice(device->name());
  dev.type(device->type());
  dev.x(device->position().x());
  dev.y(device->position().y());
  // struct TopologyUpdate update;
  // update.type = TopologyUpdate::ADD_DEV;
  // update.detail = TopologyUpdate::AddDevDetail{device->name()};
  // // struct TopologyUpdate update {
  // //   .type = TopologyUpdate::Type::ADD_DEV, .detail.addDevDetail = {.name =
  // device->name() }
  // // };
  // server_->topologyUpdatesQueue_.write(std::move(update));
  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::AddLink(
    grpc::ServerContext* ctx, const proto::Link* link, proto::Response* resp) {
  server_->networks_.at(link->network())
      .addLink(link->srcdev(), link->srcinf(), link->dstdev(), link->dstinf());
  // TopologyUpdate update;
  // update.type = TopologyUpdate::ADD_LINK;
  // // TopologyUpdate::AddLinkDetail detail = {.srcDev=link->srcdev(),
  // .srcPort=link->srcinf(), .dstDev=link->dstdev(), .dstPort=link->dstinf()};
  // update.detail = TopologyUpdate::AddLinkDetail{link->srcdev(),
  // link->srcinf(), link->dstdev(), link->dstinf()};
  // server_->topologyUpdatesQueue_.write(std::move(update));

  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::AddRoute(
    grpc::ServerContext* ctx,
    const proto::Route* route,
    proto::Response* resp) {
  XLOG(INFO) << "add route";
  // pipeline_.blockingWrite(std::move(dpu));
  // server_->routeUpdatesQueue_.write("route->");
  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::AddRoutes(
    grpc::ServerContext* ctx,
    const proto::Routes* routes,
    proto::Response* resp) {
  XLOG(INFO) << "add routes";
  auto dpu = std::make_shared<RouteUpdate>();
  dpu->device = &server_->networks_.at(routes->network())
                     .nameToDevice()
                     .at(routes->devname());

  for (int i = 0; i < routes->routes_size(); ++i) {
    auto rt = std::make_shared<Route>();
    rt->match = routes->routes(i).addr();
    // XLOG(INFO) << routes->routes(i).addr();
    // XLOG(INFO) << rt->match.to_ulong();
    rt->prefixLength = routes->routes(i).prefixlength();
    rt->priority = routes->routes(i).prefixlength();
    auto& port = server_->networks_.at(routes->network())
                     .nameToDevice()
                     .at(routes->devname())
                     .getOrCreatePort(routes->routes(i).port());
    rt->port = &port;
    dpu->added.emplace_back(rt);
    dpu->device->routes().push_back(*rt.get());
  }
  std::sort(dpu->added.begin(), dpu->added.end(), [&](auto a, auto b) {
    return a->priority > b->priority;
  });

  server_->pipeline_.blockingWrite(std::move(dpu));

  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::GetNetwork(
    grpc::ServerContext* ctx,
    const proto::GetNetworkRequest* req,
    proto::Network* resp) {
  auto& network = server_->networks_.at(req->name());
  for (auto& [name, dev] : network.nameToDevice()) {
    auto device = resp->add_devices();
    device->set_name(name);

    for (auto& [pname, port] : dev.ports()) {
      if (port.peer != nullptr) {
        auto link = resp->add_links();
        link->set_srcdev(name);
        link->set_srcinf(pname);
        link->set_dstdev(port.peer->device->name());
        link->set_dstinf(port.peer->name);
      }
    }
  }
  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::GetNetworks(
    grpc::ServerContext* ctx,
    const proto::EmptyRequest* req,
    proto::Networks* resp) {
  XLOG(INFO) << "GetNetworks";
  for (auto& [netName, network] : server_->networks_) {
    auto net = resp->add_networks();
    net->set_name(netName);
    net->set_token(network.token());
    int numRules = 0;
    auto it = network.nameToDevice().begin();
    while (it != network.nameToDevice().end())
    {
      numRules += it->second.routes().size();
      it++;
    }
    
    net->set_numrules(numRules);
    net->set_numaps(network.aps().size());

    for (auto& [name, dev] : network.nameToDevice()) {
      auto device = net->add_devices();
      device->set_name(name);
      device->set_type(dev.type());
      proto::Position pos;
      pos.set_x(dev.x());
      pos.set_y(dev.y());
      device->mutable_position()->CopyFrom(pos);

      for (auto& [pname, port] : dev.ports()) {
        if (port.peer != nullptr) {
          auto link = net->add_links();
          link->set_srcdev(name);
          link->set_srcinf(pname);
          link->set_dstdev(port.peer->device->name());
          link->set_dstinf(port.peer->name);
        }
      }
    }
  }
  return grpc::Status::OK;
}

grpc::Status
ServiceImpl::GetPath(
    grpc::ServerContext* ctx,
    const proto::GetPathRequest* req,
    proto::GetPathResponse* resp) {
  XLOG(INFO) << "GetPath";
  std::bitset<32> dip = req->dip();
  auto bdd = HeaderSpace::fromInt(dip, 32);
  auto& network = server_->networks_.at(req->network());
  bool isDst = false;
  auto& src = network.nameToDevice().at(req->source());

  // add first dev as first hop
  resp->add_hops(req->source());

  // source is a host, start at nexthop
  auto next = src.ports().begin()->second.peer->device;
  resp->add_hops(next->name());
  while (next->type() == "router") {
    for (auto& pair : next->eportToHs()) {
      if ((pair.second & bdd) != HeaderSpace::hsEmpty()) {
        next = pair.first->peer->device;
        resp->add_hops(next->name());
        continue;
      }
    }
  }

  return grpc::Status::OK;
}

} // namespace nexus